package org.smarthings.things.management.rest.util;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.transform.dom.DOMSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smarthings.things.management.metadata.ThingActionArgumentMetadata;
import org.smarthings.things.management.metadata.ThingActionMetadata;
import org.smarthings.things.management.metadata.ThingDataType;
import org.smarthings.things.management.metadata.ThingPropertyMetadata;
import org.smarthings.things.management.rest.resources.actions.ThingActionArgumentMetadataResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionMetadataResource;
import org.smarthings.things.management.rest.resources.properties.ThingPropertyMetadataResource;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class DOMElementUnmarshaller {

	private final static Logger logger = LoggerFactory.getLogger(DOMElementUnmarshaller.class);

	public static Object unmarshal(Element element, Class elementValueType) {
		Object object = null;
		if (element == null) {
			return null;
		}
		try {
			DOMSource source = new DOMSource(element);
			JAXBElement jaxbElement = JAXBContext.newInstance(elementValueType)
					.createUnmarshaller().unmarshal(source, elementValueType);
			object = jaxbElement.getValue();
		} catch (JAXBException e) {
			e.printStackTrace();
			logger.error("Error unmarshaling element " + element, e);
		}
		return object;
	}

	public static ThingPropertyMetadata unmarshalThingPropertyMetadata(
			ThingPropertyMetadataResource resource) {
		// List<Element> metadata = resource.getMetadata();
		// Map<String, String> extracted = new HashMap<>(3);
		// for (Element element : metadata) {
		// String property = (String) unmarshal(element, String.class);
		// extracted.put(element.getNodeName(), property);
		// }
		// return new ThingPropertyMetadata(extracted.get("description"), extracted.get("type"),
		// extracted.get("unit"));

		return new ThingPropertyMetadata(resource.getDesctiption(), resource.getUnit(),
				resource.getType(), resource.getMin(), resource.getMax(), resource.getAccess());
	}

	public static Object unmarshalPropertyValue(Element element, ThingPropertyMetadata metadata) {
		// ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Class elementValueType = String.class;
		if (metadata != null) {
			ThingDataType type = metadata.getType();
			if (type != null) {
				elementValueType = type.javaType();
			}
			// String type = metadata.getType();
			// if (type != null) {
			// try {
			// elementValueType = loader.loadClass(type);
			// } catch (ClassNotFoundException e) {
			// logger.error("Error loading class " + type
			// + "! Using java.lang.String for property value type!", e);
			// e.printStackTrace();
			// }
			// } else {
			// logger.warn("No type declared in property metadata! Using java.lang.String for property value type!");
			// }
		} else {
			logger.warn("No property metadata! Using java.lang.String for property value type!");
		}
		return unmarshal(element, elementValueType);
	}

	public static Object unmarshalPropertyValue(String element, ThingPropertyMetadata metadata)
			throws InstantiationException, IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, NoSuchMethodException, SecurityException {
		if (element == null) {
			return null;
		}
		// ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Class elementValueType = String.class;
		if (metadata != null) {
			ThingDataType type = metadata.getType();
			if (type != null) {
				elementValueType = type.javaType();
			}
			// String type = metadata.getType();
			// if (type != null) {
			// try {
			// elementValueType = loader.loadClass(type);
			// } catch (ClassNotFoundException e) {
			// logger.error("Error loading class " + type
			// + "! Using java.lang.String for property value type!", e);
			// e.printStackTrace();
			// }
			// } else {
			// logger.warn("No type declared in property metadata! Using java.lang.String for property value type!");
			// }
		} else {
			logger.warn("No property metadata! Using java.lang.String for property value type!");
		}
		return elementValueType.getConstructor(String.class).newInstance(element);
	}

	public static ThingActionMetadata unmarshalThingActionMetadata(
			ThingActionMetadataResource resource) {
		String description = resource.getDescription();
		List<ThingActionArgumentMetadata> arguments = null;
		List<ThingActionArgumentMetadataResource> resources = resource.getArguments();
		if (resources != null && resources.size() > 0) {
			arguments = new ArrayList<ThingActionArgumentMetadata>(resources.size());
			for (ThingActionArgumentMetadataResource argumentr : resources) {
				// List<Element> metadata = argumentr.getMetadata();
				// Map<String, String> extracted = new HashMap<>(5);
				// for (Element element : metadata) {
				// extracted.put(element.getNodeName(), (String) unmarshal(element, String.class));
				// }
				// ThingActionArgumentMetadata argument = new ThingActionArgumentMetadata(
				// extracted.get("alias"), extracted.get("description"),
				// extracted.get("type"), extracted.get("resolution"));
				ThingActionArgumentMetadata argument = new ThingActionArgumentMetadata(
						argumentr.getAlias(), argumentr.getDescription(),
						argumentr.getResolution(), argumentr.getType(), argumentr.getMin(),
						argumentr.getMax());
				arguments.add(argument);
			}
		}
		return new ThingActionMetadata(description, arguments);
	}

	public static Map<String, Object> unmarshalThingActionArguments(List<Element> arguments,
			ThingActionMetadata metadata) {
		if (arguments == null || arguments.size() == 0) {
			return null;
		}
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Class argumentValueType = String.class;
		Map<String, Object> objects = new HashMap<String, Object>(arguments.size());
		if (metadata != null) {
			List<ThingActionArgumentMetadata> argumentsMetadata = metadata.getArguments();
			if (argumentsMetadata != null) {
				Iterator<ThingActionArgumentMetadata> argumentsMetadataIterator = argumentsMetadata
						.iterator();
				while (argumentsMetadataIterator.hasNext()) {
					ThingActionArgumentMetadata argumentMetadata = argumentsMetadataIterator.next();
					Element argument = findArgument(argumentMetadata.getAlias(), arguments);
					if (argument != null) {
						ThingDataType type = argumentMetadata.getType();
						if (type != null) {
							argumentValueType = type.javaType();
						}
						// String type = argumentMetadata.getType();
						// if (type != null) {
						// try {
						// argumentValueType = loader.loadClass(type);
						// } catch (ClassNotFoundException e) {
						// logger.error("Error loading class " + type
						// + "! Using java.lang.String for argument value type!", e);
						// e.printStackTrace();
						// }
						// } else {
						// logger.warn("No type declared in metadata! Using java.lang.String for argument value type!");
						// }
						objects.put(argument.getNodeName(), unmarshal(argument, argumentValueType));
					}
				}

			} else {
				logger.warn("No arguments metadata! Using java.lang.String for argument value type!");
			}
		} else {
			logger.warn("No action metadata! Using java.lang.String for argument value type!");
		}
		return objects;
	}

	public static Map<String, Object> unmarshalThingActionArguments(Map<String, String> arguments,
			ThingActionMetadata metadata) {
		if (arguments == null || arguments.size() == 0) {
			return null;
		}
		Map<String, Object> objects = new HashMap<String, Object>(arguments.size());
		for (String argumentName : arguments.keySet()) {
			String argumentValue = arguments.get(argumentName);
			Class argumentType = findArgumentType(argumentName, metadata);
			try {
				objects.put(argumentName,
						argumentType.getConstructor(String.class).newInstance(argumentValue));
			} catch (Exception e) {
				e.printStackTrace();
			}

		}

		return objects;
	}

	private static Element findArgument(String name, List<Element> arguments) {
		Iterator<Element> iterator = arguments.iterator();
		while (iterator.hasNext()) {
			Element element = iterator.next();
			if (name.equals(element.getLocalName())) {
				return element;
			}
		}
		return null;
	}

	private static Class findArgumentType(String name, ThingActionMetadata actionMetadata) {
		Class argumentType = String.class;
		List<ThingActionArgumentMetadata> argumentsMetadata = actionMetadata.getArguments();
		for (ThingActionArgumentMetadata argumentMetadata : argumentsMetadata) {
			if (name.equals(argumentMetadata.getAlias())) {
				ThingDataType type = argumentMetadata.getType();
				if (type != null) {
					argumentType = type.javaType();
				}
				// ClassLoader loader = Thread.currentThread().getContextClassLoader();
				// String type = argumentMetadata.getType();
				// if (type != null) {
				// try {
				// argumentType = loader.loadClass(type);
				// } catch (Exception e) {
				// e.printStackTrace();
				// }
				// }
				break;
			}
		}
		return argumentType;
	}

	private static void printDOMNode(Node element, int offset) {
		if (element == null) {
			System.out.println(offset(offset) + "<null>");
			return;
		}
		System.out.println(offset(offset) + "name : " + element.getNodeName());
		System.out.println(offset(offset) + "value : " + element.getNodeValue());
		Node child = element.getFirstChild();
		while (child != null) {
			offset++;
			printDOMNode(child, offset);
			offset--;
			child = child.getNextSibling();
		}
	}

	private static String offset(int count) {
		StringBuilder buff = new StringBuilder();
		for (int i = 0; i < count; i++) {
			buff.append(' ');
		}
		return buff.toString();
	}
}
